[Generic表]TDRBlob内嵌Protobuf部分字段读写
[TDR Generic表][Go SDK]TDR Blob内嵌Protobuf部分字段读写
字段路径的完整语法、读写语义和限制见《TDR Blob 内嵌 Protobuf 部分字段读写》。本文介绍 Go SDK 接口与接入示例。
1. 接口说明
TDR 表的 Blob 字段(char/Byte 数组 + refer 长度字段)里如果保存的是 Protobuf 序列化数据, 可以只读取或只更新 Blob 中指定的 PB 字段,而不必整段回读、改完再整段回写。
三个接口对应三个命令字:
| 接口 | 命令字 | 值 | 说明 |
|---|---|---|---|
DoPBFieldGet | TcaplusApiPBFieldGetReq | 0x0067 | 读取 Blob 中指定的 PB 字段 |
DoPBFieldUpdate | TcaplusApiPBFieldUpdateReq | 0x0069 | 更新 Blob 中指定的 PB 字段,记录不存在会报错,不会自动插入 |
DoPBBatchFieldGet | TcaplusApiPBBatchFieldGetReq | 0x0075 | 多个主键共用一组字段路径批量读取 |
Blob 中 Protobuf 字段的自增(TcaplusApiPBFieldIncreaseReq)在 TDR 表上不支持。
2. 版本要求
- Go SDK 的
v0.6.36已包含本功能;使用其他版本时,请确认提供本文所述接口。 - 服务端须为 Tcapsvr 3.73.0 2026-10-10 以后的版本。旧版后端在这三个命令上返回
SVR_ERR_FAIL_TO_FIND_META,需要升级后端。
3. 准备工作
参见准备工作文档,完成使用该接口前的准备工作,并创建TDR Generic表。 service_info表service_info.xml
本特性要求表里有一个 Blob 字段 + 它的 refer 长度字段,service_info 表中对应的是:
<entry name="routeinfo_len" type="uint" defaultvalue="0" desc="路由规则信息长度" />
<entry name="routeinfo" type="char" count="1024" refer="routeinfo_len" desc="路由规则信息" />
routeinfo 是保存 PB 的 Blob 字段,routeinfo_len 是它的 refer 长度字段, 这两个字段在下面的示例里分别对应 Go 结构体的 Routeinfo 和 Routeinfo_Len。
Blob 里的 PB 定义示例如下(RouteInfo后面都是使用该proto进行示例演示):
syntax = "proto3";
package routeinfo;
message RouteInfo {
uint32 version = 1;
string strategy = 2;
Weight weight = 3;
repeated string tags = 4;
repeated Instance instances = 5;
map<int64, Instance> instance_map = 6;
map<string, string> settings = 7;
}
message Weight { uint32 cpu = 1; uint32 memory = 2; }
message Instance { string addr = 1; uint32 port = 2; uint32 weight = 3; }
该proto表定义不用上传到Tcaplus服务端,只存在于客户端
该 PB 定义与生成代码见示例目录。
4. PB部分字段操作方式
4.1. 方案1
部分字段读写的核心是字段路径,服务端没有业务的 .proto,只认数字 tag 路径,直接写PB数字tagid:
// 操作 PB RouteInfo 的 version 字段
// 操作 PB RouteInfo 的 strategy 字段
// 操作 PB RouteInfo 的 weight.cpu 字段(嵌套 message 子字段)
// 操作 PB RouteInfo 的 instance_map 字段的 map 元素 1001
// 操作 PB RouteInfo 的 settings 字段的 map 元素 region
// 操作 tdr 原生字段 filterdata
opt := &option.TDROpt{FieldNames: []string{"routeinfo.1", "routeinfo.2", "routeinfo.3.1",
"routeinfo.6[1001]", "routeinfo.7['region']", "filterdata"}}
4.2. 方案2
直接使用tagid操作字段,不够直观,维护不方便,因此我们增加了PB字段名到 tagid 映射的适配层用法,方便维护。
字段名到 tag 路径的转换用 tdrpb 包:
paths, err := tdrpb.BuildPaths(
// 操作 routeinfo 这个 Blob 里的 RouteInfo PB 字段
tdrpb.Blob("routeinfo", (*routeinfo.RouteInfo)(nil),
"version", // routeinfo.1
"strategy", // routeinfo.2
"weight.cpu", // routeinfo.3.1,嵌套 message 子字段
"instance_map[1001]", // routeinfo.6[1001],map 指定 key
"settings['region']", // routeinfo.7['region'],字符串 key 的 map value
),
// 如果 TDR 里还有别的 Blob 存放 PB Message,比如 extra.bin,可以再加一个分组
// tdrpb.Blob("extra.bin", (*xxx.Other)(nil), "version"),
// 操作原生 TDR 一级字段,原样透传
tdrpb.Raw("filterdata"),
)
if err != nil {
return
}
// 最终调用会转换成方案1的调用
opt := &option.TDROpt{FieldNames: paths}
Blob 中的 PB 参数只用于获取类型,不提供更新值。更新数据由增量 PB 提供,见下一节。删除 map 元素时使用 tdrpb.Pop("routeinfo", (*routeinfo.RouteInfo)(nil), "instance_map[1001]"),生成 POP routeinfo.6[1001];不通过 opt.Operation 传递。
FieldNames 中无需手动加入 routeinfo、routeinfo_len:SDK 自动补齐一级字段及所需 refer。一次请求也可以通过多个 Blob 分组选择不同 Blob 中的字段。
5. 完整示例代码
示例代码见示例目录, 本特性相关的示例文件为 pbfieldprepare.go、pbfieldget.go、pbfieldupdate.go、pbbatchfieldget.go, 入口在 main.go 中。下面的准备数据、5.1、5.2、5.3 分别对应示例文件里的 pbFieldPrepareExample、pbFieldGetExample、pbFieldUpdateExample、pbBatchFieldGetExample, 响应处理对应 5.4。
Blob 里必须是合法的 PB 编码,普通写入的裸字符串服务端解析不了。先整段写一份完整 PB(对应示例文件的 pbFieldPrepareExample):
func pbFieldPrepareExample() {
// 1. 构造完整 PB
full := &routeinfo.RouteInfo{Version: 1, Strategy: "round_robin", /* ... */}
// 增量/整段 PB 都用 partial 语义序列化,proto2 缺 required 字段时也能正常处理
buf, err := tdrpb.MarshalPartial(full)
if err != nil {
fmt.Println(err.Error())
return
}
// 2. 填主键,Blob 和它的 refer 长度字段一起整段写入
data := service_info.NewService_Info()
data.Gameid = "dev"
data.Envdata = "oa"
data.Name = "com"
data.Routeinfo = buf
data.Routeinfo_Len = uint32(len(buf))
// 3. 用 Replace 而不是 Insert,示例可以反复跑
if err = client.DoReplace(TableName, data, nil); err != nil {
fmt.Println(err.Error())
return
}
}
5.1 读取部分字段
对应示例文件的 pbFieldGetExample、pbFieldGetContainerExample。
func pbFieldGetExample() {
// 1. 只填主键,value 字段由服务端按字段路径回填,业务不需要预先序列化 Blob
data := service_info.NewService_Info()
data.Gameid = "dev"
data.Envdata = "oa"
data.Name = "com"
/////////////////////////////////////////////////////////////////////
// 2. 唯一与适配层相关的步骤:按 PB 字段名构造字段路径
opt, err := &option.TDROpt{}, error(nil)
if opt.FieldNames, err = tdrpb.BuildPaths(
// routeinfo 这个 Blob 里的 RouteInfo PB 字段
tdrpb.Blob("routeinfo", (*routeinfo.RouteInfo)(nil),
"version", // routeinfo.1
"strategy", // routeinfo.2
"weight.cpu", // routeinfo.3.1
),
// 原生 TDR 一级字段,原样透传
tdrpb.Raw("filterdata"),
); err != nil {
fmt.Println(err.Error())
return
}
// 或者使用方案1的tagid方式调用
// opt.FieldNames = []string{"routeinfo.1", "routeinfo.2", "routeinfo.3.1", "filterdata"}
///////////////////////////////////////////////////////////////////
// 3. 发起请求,结果直接回填到 data
if err = client.DoPBFieldGet(TableName, data, opt); err != nil {
fmt.Println(err.Error())
return
}
// 4. Routeinfo_Len 是本次部分 PB 的长度,不是记录里完整 Blob 的长度
partial := &routeinfo.RouteInfo{}
if err = tdrpb.UnmarshalPartial(data.Routeinfo, data.Routeinfo_Len, partial); err != nil {
fmt.Println(err.Error())
return
}
// 5. 只有请求过的字段有值,未请求的字段是未设置状态,返回的部分记录不能当完整记录用
fmt.Println(partial.GetVersion(), partial.GetStrategy(), partial.GetWeight().GetCpu(), data.Filterdata)
}
BuildPaths 最终生成的是 routeinfo.1、routeinfo.2、routeinfo.3.1 和 filterdata,服务端只回填这四个路径对应的值。
与 5.2 的更新不同,读取不需要业务构造增量 PB:只填主键,Blob 内容由服务端按字段路径写回,响应里直接解析即可。filterdata 是原生 TDR 字段,不经过 PB 解析,和三个 PB 路径一起传入同一次 SetFieldNames。
map 指定 key 和 repeated 指定下标(pbFieldGetContainerExample)写法相同,只是路径不同:
opt, err := &option.TDROpt{}, error(nil)
if opt.FieldNames, err = tdrpb.BuildPaths(
tdrpb.Blob("routeinfo", (*routeinfo.RouteInfo)(nil),
"instance_map[1001].addr", // routeinfo.6[1001].1,map 元素的子字段
"instances[0]", // routeinfo.5#[0],repeated 下标用 #[]
"settings", // routeinfo.7,整组读取
),
); err != nil {
fmt.Println(err.Error())
return
}
// 读不存在的 map key 返回 COMMON_ERR_ELEMENT_NOT_EXIST,不是一次成功的默认值读取
if err = client.DoPBFieldGet(TableName, data, opt); err != nil {
fmt.Println(err.Error())
return
}
5.2 更新部分字段
对应示例文件的 pbFieldUpdateExample(部分更新)、pbFieldUpdateMapExample(map 元素覆盖与删除)。
func pbFieldUpdateExample() {
// 1. 只填主键
data := service_info.NewService_Info()
data.Gameid = "dev"
data.Envdata = "oa"
data.Name = "com"
// 2. 业务构造增量 PB,只带本次要写的字段
delta := &routeinfo.RouteInfo{Version: 3, Strategy: "weighted_round_robin"}
// 3. 增量 PB 序列化到 TDR Blob
data.Routeinfo = make([]byte, 1024) // Init 不会给 Blob 分配空间,tdr_count 是 1024
n, err := tdrpb.MarshalPartialTo(data.Routeinfo, delta)
if err != nil {
fmt.Println(err.Error())
return
}
data.Routeinfo_Len = uint32(n)
/////////////////////////////////////////////////////////////////////
// 4. 唯一与适配层相关的步骤:按 PB 字段名构造字段路径
opt := &option.TDROpt{}
if opt.FieldNames, err = tdrpb.BuildPaths(
tdrpb.Blob("routeinfo", (*routeinfo.RouteInfo)(nil),
"version", // routeinfo.1
"strategy", // routeinfo.2
),
); err != nil {
fmt.Println(err.Error())
return
}
// 或者使用方案1的tagid方式调用
// opt.FieldNames = []string{"routeinfo.1", "routeinfo.2"}
///////////////////////////////////////////////////////////////////
// 5. 增量只带本次要写的字段,Blob 里其余字段服务端原样保留
if err = client.DoPBFieldUpdate(TableName, data, opt); err != nil {
fmt.Println(err.Error())
return
}
}
BuildPaths 最终生成的是 routeinfo.1 和 routeinfo.2,只有这两个路径允许被修改, Blob 中其余字段服务端原样保留。路径决定修改范围,增量提供新值。
删除 map 元素改用 Pop 分组,此时不需要增量,Blob 留空、长度填 0:
opt, err := &option.TDROpt{}, error(nil)
if opt.FieldNames, err = tdrpb.BuildPaths(
// POP routeinfo.6[1001]
tdrpb.Pop("routeinfo", (*routeinfo.RouteInfo)(nil), "instance_map[1001]"),
); err != nil {
fmt.Println(err.Error())
return
}
// 删除不提供新值,Routeinfo 留空、Routeinfo_Len 为 0
data := service_info.NewService_Info()
data.Gameid = "dev"
data.Envdata = "oa"
data.Name = "com"
if err = client.DoPBFieldUpdate(TableName, data, opt); err != nil {
fmt.Println(err.Error())
return
}
TDR 的 tinyint 数组会被生成为 []int8,这种表换用 tdrpb.MarshalPartialToInt8 / tdrpb.UnmarshalPartialInt8,参数含义一致。
5.3 批量读取
对应示例文件的 pbBatchFieldGetExample。
func pbBatchFieldGetExample() {
// 1. 每条记录只填主键,单次最多 1024 条
var dataSlice []record.TdrTableSt
for i := 0; i < 10; i++ {
data := service_info.NewService_Info()
data.Gameid = "dev"
data.Envdata = "oa"
data.Name = fmt.Sprintf("%d", i)
dataSlice = append(dataSlice, data)
}
/////////////////////////////////////////////////////////////////////
// 2. 唯一与适配层相关的步骤:所有主键共用同一组字段路径,只构造一次
opt, err := &option.TDROpt{}, error(nil)
if opt.FieldNames, err = tdrpb.BuildPaths(
tdrpb.Blob("routeinfo", (*routeinfo.RouteInfo)(nil), "version", "strategy"),
tdrpb.Raw("filterdata"),
); err != nil {
fmt.Println(err.Error())
return
}
// 或者使用方案1的tagid方式调用
// opt.FieldNames = []string{"routeinfo.1", "routeinfo.2", "filterdata"}
///////////////////////////////////////////////////////////////////
// 3. 请求级成功不等于每条记录都成功,需要逐条检查 opt.BatchResult
if err = client.DoPBBatchFieldGet(TableName, dataSlice, opt); err != nil {
fmt.Printf("batch field get: %s\n", err)
// 可能只有部分记录失败,继续检查已经返回的逐条结果。
}
if len(opt.BatchResult) != len(dataSlice) {
return // 请求构造失败等情况下,逐条结果尚未建立。
}
// 4. 按主键回填到 dataSlice 的对应下标,逐条解析部分 PB
for i, data := range dataSlice {
if opt.BatchResult[i] != nil {
fmt.Printf("record %d failed: %s\n", i, opt.BatchResult[i].Error())
continue
}
row := data.(*service_info.Service_Info)
partial := &routeinfo.RouteInfo{}
if err = tdrpb.UnmarshalPartial(row.Routeinfo, row.Routeinfo_Len, partial); err != nil {
fmt.Println(err.Error())
continue
}
fmt.Printf("record %d version %d, filterdata %s\n", i, opt.BatchVersion[i], row.Filterdata)
}
}
BuildPaths 最终生成的是 routeinfo.1、routeinfo.2 和 filterdata。
与 5.1 的读取一样只填主键,区别是一次请求带多条;所有主键共用同一组字段路径, BuildPaths 只调用一次,单次最多 1024 条主键。 响应不保证顺序,按主键回填到 dataSlice 的对应下标;每条记录各自返回结果, 用 opt.BatchResult[i] 判断这一条是否成功,版本看 opt.BatchVersion[i]。 分包由服务端通过响应里的 LeftNum 驱动,SDK 已经收完所有分包,不需要设置 MultiFlag。
5.4 响应与逐条结果
Go 是同步接口,没有独立的响应对象:成功时结果直接回填到传入的 data / dataSlice, 失败时通过接口返回值和 opt.BatchResult 给出错误。三个接口共用同一套处理逻辑,有三点需要注意:
data.Routeinfo_Len是本次部分 PB 的长度,不是记录里完整 Blob 的长度;- 解析出的
partial只包含本次请求的字段,不能再拿去整段覆盖 Blob; - 批量请求里每条记录各自返回结果,
opt.BatchResult[i]才是这一条的结果, 接口返回值是请求级结果,成功也不代表每条记录都成功。
6. option 支持情况
opt 必填且 opt.FieldNames 不能为空。不支持的选项会返回错误;批量 Condition 返回 GEN_ERR_INVALID_ARGUMENTS。
| option | FieldGet | FieldUpdate | BatchFieldGet |
|---|---|---|---|
FieldNames | 必填 | 必填 | 必填 |
Condition | 支持 | 支持 | 不支持(请求体没有条件字段) |
Timeout / Ctx / Flags / UserBuff | 支持 | 支持 | 支持 |
Version | 支持,入参兼出参 | 支持 | 拒绝,版本只从 BatchVersion 出参 |
VersionPolicy | 拒绝 | 支持 | 拒绝 |
ResultFlagForSuccess / ResultFlagForFail | 拒绝 | 支持 | 拒绝 |
BatchResult / BatchVersion | 不适用 | 不适用 | 支持 |
MultiFlag / Operation / ResultFlag / IncField / TTL / Limit / Offset / ExpireTime 等 | 拒绝 | 拒绝 | 拒绝 |
Version 在 DoPBFieldGet 上既是入参也是出参,响应回来时被改写为记录的当前版本。
7. 错误码
本地检查(接口返回值):
| 场景 | 错误码 |
|---|---|
opt 为 nil、FieldNames 为空、使用了不支持的 option | ParameterInvalid |
| 路径为空或超过 1023 字节 | API_ERR_OVER_MAX_FIELD_NAME_LEN |
| 补齐后字段数超过 256 | API_ERR_OVER_MAX_VALUE_FIELD_NUM |
路径重复或父子路径冲突、未先 SetData | API_ERR_PARAMETER_INVALID |
| 路径的一级字段不是本表的 value 字段 | API_ERR_FIELD_NOT_EXSIST |
| PB 字段不存在 | API_ERR_FIELD_NOT_EXSIST |
| map / repeated / 嵌套 message 类型不匹配 | API_ERR_FIELD_TYPE_NOT_MATCH |
| Blob 容量不足 | API_ERR_OVER_MAX_FIELD_VALUE_LEN |
发送后的结果看接口返回值;批量还要看每条 opt.BatchResult。常见服务端错误码:
| 错误码 | 含义 |
|---|---|
TXHDB_ERR_RECORD_NOT_EXIST | 记录不存在 |
COMMON_ERR_ELEMENT_NOT_EXIST | 指定的 map key 或 repeated 下标不存在 |
COMMON_ERR_CONDITION_NOT_MATCHED | 条件不成立 |
SVR_ERR_FAIL_INVALID_VERSION | 版本校验失败 |
详见错误码含义和处理方法。
8. 注意事项
- 只支持 Generic 表,List 表只能由服务端拒绝,SDK 本地拿不到表类型。
- Blob 里必须是当前业务 descriptor 对应的 PB 编码,历史数据的 schema 兼容性由业务保证。
- 返回的
routeinfo_len是本次部分 PB 的长度,不是记录中完整 Blob 的长度; 解析出的 PB 对象只包含本次请求的字段,不能直接用于整段 Blob 覆盖。 - SDK 不做 PB 编解码,增量 PB 的构造、Blob 的写入、响应 Blob 的解析都由业务负责,
tdrpb只提供MarshalPartial/MarshalPartialTo/UnmarshalPartial等便利函数。 - 路径里的一级字段名取 TDR 表定义中的字段名(
tdr_field),不是 Go 结构体字段名。 - 字段名使用
.proto中的原始 field name,不使用 JSON name。 - 嵌套结构体里的 Blob(如
extra.bin)也支持,SDK 自动补齐其所属的一级结构体字段。